DLO-JZ Optimisation de l'apprentissage - Jour 2 matin¶

Optimisation système d'une boucle d'apprentissage Resnet-50.

car

Objet du notebook¶

Le but de ce notebook est d'optimiser un code d'apprentissage d'un modèle Resnet-50 sur Imagenet pour Jean Zay en implémentant :

  • TP 1 : la distribution (Data Parallelism)
  • TP 2 : Imagenet Race - Test Tour

Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.

Les directives de modification seront marquées par l'étiquette TODO :, dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, juin 2023


Environnement de calcul¶

Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :

In [1]:
!module list
Currently Loaded Modulefiles:
 1) cuda/11.2                5) openmpi/4.1.1-cuda   9) sparsehash/2.0.3        
 2) nccl/2.9.6-1-cuda        6) intel-mkl/2020.4    10) libjpeg-turbo/2.1.3     
 3) cudnn/8.1.1.33-cuda      7) magma/2.5.4-cuda    11) pytorch-gpu/py3/1.11.0  
 4) gcc/8.5.0(8.3.1:8.4.1)   8) sox/14.4.2          
>

Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation et la compétition.

In [2]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter
MODULE = 'pytorch-gpu/py3/1.11.0'
image_size = 176
account = 'for@v100'
name = 'pseudo'   ## Pseudonyme à choisir

Creation d'un repertoire checkpoints si cela n'a pas déjà été fait.

In [3]:
!mkdir checkpoints
mkdir: cannot create directory ‘checkpoints’: File exists

Gestion de la queue SLURM¶

Cette partie permet d'afficher et de gérer la queue SLURM.

Pour afficher toute la queue utilisateur :

In [4]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            247975   gpu_p13   pseudo  cfor132  R       1:17      1 r6i3n3

 Done!

Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.

Si vous voulez arrêter des jobs dans la queue:

  • Annuler tous vos jobs dans la queue (décommenter la ligne suivante) : !scancel -u $USER
  • Annuler un job dans votre queue (décommenter la ligne suivante et ajouter le numéro du job à la fin de la ligne)
In [5]:
#!scancel -u $USER

Debug¶

Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.

Il est nécessaire dans la cellule suivante (en décommentant) d'indiquer le jobid correspondant sous le format suivant.

*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée."

In [6]:
jobid = ['1493206']

Fichier de sortie :

In [7]:
%cat {search_log(contains=jobid[0])[0]}
>>> Training on  1  nodes and  1  processes
model: Resnet-50
number of parameters: 25557032
global batch size: 128 - mini batch size: 128
Optimizer: SGD (
Parameter Group 0
    dampening: 0
    lr: 0.1
    maximize: False
    momentum: 0.9
    nesterov: False
    weight_decay: 0.0
)
DATALOADER 10 True True True 3 False 
image batch shape : torch.Size([128, 3, 176, 176])
>>> Training complete in: 0:05:53.561484
>>> Training performance time: min 4.501930236816406 avg 4.766597509384155 seconds (+/- 0.14579007426206964)
>>> Loading performance time: min 0.00013065338134765625 avg 0.00015664100646972656 seconds (+/- 1.434471526857817e-05)
>>> Forward performance time: 1.8760071092722368 seconds (+/- 0.09052225345171229)
>>> Backward performance time: 2.901484557560512 seconds (+/- 0.08457200485922346)
>>> Peak Power during training: 62.22 W)
>>> Validation time estimation: 0:17:49.429900
>>> Sortie trace #####################################
>>>JSON {"GPU process - Forward/Backward": [6.2423787117004395, 5.0220746994018555, 4.957103490829468, 5.0125415325164795, 4.943894863128662, 4.966577529907227, 5.019651651382446, 4.99901819229126, 4.964149236679077, 4.885366678237915, 4.904692649841309, 4.781780242919922, 4.951511859893799, 4.84772253036499, 4.766597509384155, 4.684057712554932, 4.787169456481934, 4.795409679412842, 4.917107105255127, 4.681482791900635, 4.62136173248291, 4.676070213317871, 4.717148303985596, 4.702852964401245, 4.64032506942749, 4.7434868812561035, 4.692547082901001, 4.687806129455566, 4.651872873306274, 4.681207180023193, 4.635786294937134, 4.501930236816406, 4.82633376121521, 4.589520454406738, 4.554682970046997, 4.706907749176025, 4.620109558105469, 4.52482008934021, 4.774549961090088, 4.610376834869385, 4.6381542682647705, 4.710811138153076, 5.109354257583618, 4.724546909332275, 4.806366920471191, 4.937008380889893, 4.881795883178711, 4.729339838027954, 4.814107179641724, 4.803519010543823], "CPU process - Dataloader": [3.979201555252075, 0.0001895427703857422, 0.00014591217041015625, 0.00017380714416503906, 0.00013375282287597656, 0.00013637542724609375, 0.0001621246337890625, 0.0001347064971923828, 0.00015163421630859375, 0.00015854835510253906, 0.00017547607421875, 0.0001685619354248047, 0.00014591217041015625, 0.0001819133758544922, 0.00013637542724609375, 0.0001373291015625, 0.0001327991485595703, 0.00013065338134765625, 0.0001361370086669922, 0.00015592575073242188, 0.0001666545867919922, 0.0001628398895263672, 0.00017642974853515625, 0.00017499923706054688, 0.0001685619354248047, 0.00014543533325195312, 0.00013685226440429688, 0.00013947486877441406, 0.00013518333435058594, 0.00015878677368164062, 0.00015592575073242188, 0.0001609325408935547, 0.0001652240753173828, 0.0001544952392578125, 0.0001742839813232422, 0.00015854835510253906, 0.00015926361083984375, 0.00015854835510253906, 0.0001609325408935547, 0.00016069412231445312, 0.00016307830810546875, 0.00015592575073242188, 0.00015592575073242188, 0.00015783309936523438, 0.00017976760864257812, 0.00015854835510253906, 0.00015735626220703125, 0.00016188621520996094, 0.0001595020294189453, 0.000164031982421875]}
>>> Number of batch per epoch: 10010
Max Memory Allocated 0 Bytes
Tue Feb 21 21:41:18 CET 2023

Fichier d'erreur :

In [8]:
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
Loading pytorch-gpu/py3/1.11.0
  Loading requirement: cuda/11.2 nccl/2.9.6-1-cuda cudnn/8.1.1.33-cuda gcc/8.4.1
    openmpi/4.1.1-cuda intel-mkl/2020.4 magma/2.5.4-cuda sox/14.4.2
    sparsehash/2.0.3 libjpeg-turbo/2.1.3
+ srun python -u dlojz.py -b 128 --image-size 176 --test
/gpfslocalsup/pub/anaconda-py3/2021.05/envs/pytorch-gpu-1.11.0+py3.9.12/lib/python3.9/site-packages/apex/pyprof/__init__.py:5: FutureWarning: pyprof will be removed by the end of June, 2022
  warnings.warn("pyprof will be removed by the end of June, 2022", FutureWarning)

real	6m27.615s
user	0m0.020s
sys	0m0.013s
+ date

Différence entre deux scripts¶

Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.

In [9]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz2_1.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


Garage - Mise à niveau¶

On fixe la taille d'image pour ce TP.

In [10]:
image_size = 176

On choisit le batch size optimal d'après les expériences du Jour 1.

In [11]:
## Choisir un batch size optimal
bs_optim = 512   ##TODO

TODO : Comparer votre script dlojz.py avec ce qu'il devrait être actuellement. Si il y a des divergences, veuillez les corriger (par exemple en copiant-collant la solution).

In [12]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz2_0.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante :

compare.html

In [13]:
# copier/coller la solution si nécessaire
#!cp solutions/dlojz2_0.py dlojz.py

TP2_1 : Distribution - Parallélisme de données¶

TODO : dans le script dlojz.py :

  • Importer les fonctionnalités liées à la distibution et au Data Parallelism.
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel
  • Configurer la distribution.
# configure distribution method: define rank and initialise communication backend (NCCL)
    dist.init_process_group(backend='nccl', init_method='env://',
                            world_size=idr_torch.size, rank=idr_torch.rank)
  • Associer le GPU alloué au process.
# define model
    torch.cuda.set_device(idr_torch.local_rank)
    gpu = torch.device("cuda")
  • Mettre en place la distribution du modèle.
model = DistributedDataParallel(model, device_ids=[idr_torch.local_rank])
  • Dans la partie DATALOADER,
    • Implémenter les samplers pour les train_loader et val_loader.
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset,
                                                                    num_replicas=idr_torch.size,
                                                                    rank=idr_torch.rank,
                                                                    shuffle=True)
val_sampler = torch.utils.data.distributed.DistributedSampler(val_dataset,
                                                                  num_replicas=idr_torch.size,
                                                                  rank=idr_torch.rank,
                                                                  shuffle=False)
  • Dans train_loader et val_loader, ajouter la ligne sampler=train_sampler, (ou sampler=val_sampler,) pour associer le bon sampler et basculer le shuffle= du loader à False pour ne pas avoir de conflit avec le shuffle du sampler.

  • Au tout début de la boucle d'apprentissage indiquer au sampler l'epoch, afin d'obtenir un shuffle différent à chaque epoch.

#### TRAINING ############
    for epoch in range(args.epochs):
        train_sampler.set_epoch(epoch)
  • Les métriques pour l'apprentissage doivent être échangées et moyennées.
# Metric mesurement
    _, predicted = torch.max(outputs.data, 1)
    accuracy = (predicted == labels).sum() / labels.size(0)
    dist.all_reduce(accuracy, op=dist.ReduceOp.SUM)
    accuracy /= idr_torch.size
    if idr_torch.rank == 0: accuracies.append(accuracy.item())
  • Les métriques pour la validation doivent être échangées et moyennées après la boucle de validation.
for iv, (val_images, val_labels) in enumerate(val_loader):
        ...
        ...
    dist.all_reduce(val_loss, op=dist.ReduceOp.SUM)
    dist.all_reduce(val_accuracy, op=dist.ReduceOp.SUM)

A noter : la moyenne des métriques distribuées sur les différents GPU se calcule pour la validation différemment du training. Ici la métrique est pondérée par rapport à la taille globale du dataset de validation. Il n'est donc pas nécessaire de diviser par idr_torch.size.

  • Ajouter une barrière après la boucle d'apprentissage afin d'éviter que certains process sortent de la distribution à la toute fin, alors que d'autres n'ont pas fini leur boucle de validation.
## Be sure all process finish at the same time to avoid incoherent logs at the end of process
dist.barrier()
In [14]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
Out[14]:
'dlojz.py -b 512 --image-size 176 --test'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [15]:
n_gpu = 4
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 247976
jobid = ['247976']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [16]:
#jobid = ['1493733']
In [17]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            247976   gpu_p13   pseudo  cfor132  R       1:17      1 r6i7n6

 Done!
In [18]:
controle_technique(jobid)
Train throughput: 5643.45 images/second
GPU throughput: 5752.83 images/second
epoch time: 227.17 seconds
training time estimation for 90 epochs (with validations): 6.44 hours
-----------
training step time average (fwd/bkwd on GPU): 0.355999 sec (41.7%/69.1%) +/- 0.121359
loading step time average (CPU to GPU): 0.006900 sec +/- 0.027825
-----------
ELIGIBLE to run 37 epochs

Commentaires


TP2_2 : Imagenet Racing - Test Tour¶

race

Le but de ce TP est de paramétrer l'entraînement pour participer à la course Imagenet Racing.

Les job de chaque participant durant environ 30 minutes, s'exécuteront pendant la nuit. Les résultats seront commentés le lendemain.

In [19]:
from dlojz_tools import plot_accuracy, imagenet_starter, plot_time

TODO : Chercher les bons paramètres, notamment la taille de batch par GPU batch_size et la taille d'image image_size permettant d'avoir un bon équilibre (d'après votre intuition) entre une taille d'image suffisante et un nombre d'epochs suffisant.

Le nombre d'epochs auquel vous avez le droit dépend du Throughput mesuré pendant le test. Il faudra regarder la dernière ligne du test Eligible to run X epochs pour connaître cette mesure.

Test d'occupation mémoire¶

Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [20]:
## TODO : Définir une taille d'image
image_size = 176
In [21]:
n_gpu = 1
batch_size = [32, 64, 128, 256, 512, 1024, 2048]
command = [f'dlojz.py -b {b} --image-size {image_size} --test'
          for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 247981
Submitted batch job 247982
Submitted batch job 247983
Submitted batch job 247984
Submitted batch job 247985
Submitted batch job 247986
Submitted batch job 247988
jobids = ['247981', '247982', '247983', '247984', '247985', '247986', '247988']
In [22]:
#jobids = ['1493910', '1493914', '1493916', '1493918', '1493920', '1493932', '1493937']
In [23]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            247988   gpu_p13   pseudo  cfor132  R       1:27      1 r6i7n6

 Done!
In [24]:
GPU_underthehood(jobids)
Batch size per GPU: 32 Max GPU Memory Allocated: 1.79 GB, Troughput: 641.386 images/second
Batch size per GPU: 64 Max GPU Memory Allocated: 2.11 GB, Troughput: 1072.609 images/second
Batch size per GPU: 128 Max GPU Memory Allocated: 3.78 GB, Troughput: 1415.000 images/second
Batch size per GPU: 256 Max GPU Memory Allocated: 7.11 GB, Troughput: 1560.810 images/second
Batch size per GPU: 512 Max GPU Memory Allocated: 13.77 GB, Troughput: 1693.423 images/second
Batch size per GPU: 1024 Max GPU Memory Allocated: 27.05 GB, Troughput: 1587.346 images/second
Batch size per GPU: 2048 CUDA out of memory
Memory occupancy by Model part : 0.460 +/- 0.018 GB
In [25]:
## TODO : Choisir un batch size optimal
batch_size = 512

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [26]:
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(command)
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 247989
dlojz.py -b 512 --image-size 176 --test
jobid = ['247989']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Nous vous conseillons de garder plusieurs lignes en mémoire afin de pouvoir comparer facilement vos différentes expériences.

In [27]:
#jobid = ['1494033']
In [28]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            247989   gpu_p13   pseudo  cfor132  R       0:54      1 r6i3n0

 Done!
In [29]:
controle_technique(jobid)
Train throughput: 1683.77 images/second
GPU throughput: 1700.69 images/second
epoch time: 761.11 seconds
training time estimation for 90 epochs (with validations): 21.93 hours
-----------
training step time average (fwd/bkwd on GPU): 0.301055 sec (43.0%/64.7%) +/- 0.078708
loading step time average (CPU to GPU): 0.003025 sec +/- 0.018090
-----------
ELIGIBLE to run 41 epochs

Apprentissage complet sur 32 GPU (à lancer en toute fin de journée)¶

TODO : Une fois que vous avez choisi la configuration que vous souhaitez engager pour la course, la fonction suivante permet de générer la bonne commande à soumettre à SLURM avec le bon nombre d'epochs, les bonnes configurations de taille de batch par GPU et de taille d'image, à condition d'avoir fourni le bon jobid.

In [30]:
?imagenet_starter
Signature:
imagenet_starter(
    jobid,
    lr=None,
    moment=0.9,
    weight_decay=0.0005,
    jour2=False,
)
Docstring: <no docstring>
File:      /gpfsdswork/projects/idris/for/cfor132/dlo-jz/dlojz_tools.py
Type:      function
In [31]:
#jobid = ['1292862']
In [32]:
command = imagenet_starter(jobid, weight_decay=5e-4)
command
Out[32]:
'dlojz.py -b 512 -e 41 --image-size 176 --lr 1.0 --mom 0.9 --wd 0.0005'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [33]:
n_gpu = 32
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name+'_race',
                   account=account, time_max='01:00:00', constraint='v100-32g', qos='qos_gpu-dev')
print(f'jobid = {jobid}')
batch job 0: 32 GPUs distributed on 8 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 247990
jobid = ['247990']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [34]:
#jobid = ['1494173']

Visualisation des résultats¶

In [35]:
jobids = ['1494173', jobid[0]]

Résultat de référence : image_size = 176, batch_size = 512¶

In [36]:
plot_accuracy(jobids[:1])
Resnet-50: SGD bs: 16384 is: 176 lr: 1.0 wd: 0.0005 top-1: 0.7212 >>> Training complete in: 0:30:34.607509
In [37]:
plot_time(jobids[:1])
In [42]:
display_slurm_queue(name+'_race')
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            247990   gpu_p13 pseudo_r  cfor132  R      35:03      8 r7i5n[1-8]

 Done!

Votre résultat¶

In [43]:
plot_accuracy(jobids)
Resnet-50: SGD bs: 16384 is: 176 lr: 1.0 wd: 0.0005 top-1: 0.7212 >>> Training complete in: 0:30:34.607509
Resnet-50: SGD bs: 16384 is: 176 lr: 1.0 wd: 0.0005 top-1: 0.7212 >>> Training complete in: 0:34:29.989931
In [44]:
plot_time(jobid)

Publication des Résultats sur WandB¶

Décommenter la ligne #!wandb sync --sync-all pour publier les résultats sur le dépôt WandB

In [41]:
import os
os.environ['WANDB_API_KEY']='2ecf1cc3a3fe45c17b480e66dd0f390c85763d42'
#!wandb sync --sync-all

https://wandb.ai/dlojz/Imagenet%20Race%20Cup?workspace=user-bcabot